package com.itheima.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.common.EsProperties;
import com.itheima.dto.PageResult;
import com.itheima.dto.RequestDto;
import com.itheima.mapper.HotelMapper;
import com.itheima.pojo.Hotel;
import com.itheima.pojo.HotelDoc;
import com.itheima.service.HotelService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.lucene.search.function.CombineFunction;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.BucketOrder;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestBody;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class HotelServiceImpl extends ServiceImpl<HotelMapper, Hotel> implements HotelService {
    @Autowired
    private RestHighLevelClient client;
    @Autowired
    private EsProperties es;

    /**
     * 根据输入内容搜索对应酒店(分页和搜索)
     *
     * @param
     * @return
     */
    @Override
    public PageResult search(RequestDto params) {
        //1.创建查找对象SearchRequest， 设置索引库（“index”）
        SearchRequest request = new SearchRequest(es.getIndexName());

        //设置查询相关请求条件
        requestCondition(params, request);

        try {
            //4.处理结果集对象
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            //获取数据总条数
            long total = response.getHits().getTotalHits().value;
            //获取hits数组
            SearchHit[] hits = response.getHits().getHits();
            //创建数组存储酒店对象
            List<HotelDoc> list = new ArrayList<>();
            for (SearchHit hit : hits) {
                //封装基本参数
                String json = hit.getSourceAsString();
                HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);

                //获取sort中数组数据（酒店距离值）
                Object[] sortValues = hit.getSortValues();
                //将数组数据转化成字符串,不为空，则设置距离值
                if (sortValues != null && sortValues.length > 0) {
                    //不能使用这个工具，因为前端需要Object类型数据
                    //String join = StringUtils.join(sortValues);
                    hotelDoc.setDistance(sortValues[0]);
                }

                //设置高亮
                Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                HighlightField highlightField = highlightFields.get("name");
                if (highlightField != null) {
                    //获取键：name的 值：数组结果
                    Text[] fragments = highlightField.getFragments();
                    if (fragments != null && fragments.length > 0) {
                        //转化成字符串形式（字数超过100数组会切割）
                        String joinName = StringUtils.join(fragments);
                        //设置高亮词
                        hotelDoc.setName(joinName);
                    }
                }

                list.add(hotelDoc);
            }
            return new PageResult(total, list);
        } catch (IOException e) {
            log.error("响应结果异常，异常信息为{}", e.getMessage());
        }
        return new PageResult();
    }


    /**
     * 对输入内容进行过滤分组（使用聚合）
     * @param params 请求参数
     * @return
     */
    @Override
    public Map<String, List<String>> filters(RequestDto params) {
        //1.获取请求对象
        SearchRequest request = new SearchRequest(es.getIndexName());

        //设置查询相关条件
        requestCondition(params, request);

        //3.不显示酒店数据结果集
        request.source().size(0);

        //调用方法，对es中数据进行分组（聚合）
        aggregationGroup(request, "cityAgg", "city");
        aggregationGroup(request, "starAgg", "starName");
        aggregationGroup(request, "brandAgg", "brand");


        try {
            //5.执行查询请求，获取响应结果
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            //处理结果集
            //获取聚合对象
            Aggregations aggregations = response.getAggregations();

            //调用方法，将获取分组后的数据并存入集合
            List<String> cities = getBucketKey(aggregations, "cityAgg");
            List<String> starNames = getBucketKey(aggregations, "starAgg");
            List<String> brands = getBucketKey(aggregations, "brandAgg");

            //添加到Map集合并返回
            Map<String, List<String>> map = new HashMap<>();
            map.put("city", cities);
            map.put("starName", starNames);
            map.put("brand", brands);
            return map;

        } catch (IOException e) {
            log.error("对输入内容进行过滤分组出错，错误为{}", e.getMessage());
        }

        return new HashMap<String, List<String>>();
    }

    /**
     * 搜索框自动补全功能（数据联想）
     * @param key 关键字（可以是首字母）
     * @return
     */
    @Override
    public List<String> suggestion(String key) {
        //获取请求对象
        SearchRequest request = new SearchRequest(es.getIndexName());

        //设置自动补全查询
        request.source().suggest(
                new SuggestBuilder().addSuggestion(
                        //设置自动补全的标识
                        "mySuggest",
                        //补全字段
                        SuggestBuilders.completionSuggestion("suggestion")
                                //输入框输入的数据（需要补全的数据）
                                .prefix(key)
                                //获取前10条结果
                                .size(10)
                                //跳过重复数据
                                .skipDuplicates(true)));

        //响应数据集
        try {
            SearchResponse response = client.search(request, RequestOptions.DEFAULT);
            //获取自动补全结果集
            Suggest suggest = response.getSuggest();
            //创建集合存储补全的数据
            List<String> list = new ArrayList<>();
            //使用多态子类接收
            CompletionSuggestion mySuggest = suggest.getSuggestion("mySuggest");
            if (mySuggest!=null){
                List<CompletionSuggestion.Entry.Option> options = mySuggest.getOptions();
                if (options!=null&&options.size()>0){
                    for (CompletionSuggestion.Entry.Option option : options) {
                        //获取可以补全的关键词
                        Text text = option.getText();
                        list.add(text.toString());
                    }
                }
            }
            return list;
        } catch (IOException e) {
            log.error("搜索框自动补全功能，错误为{}", e.getMessage());
        }

        return new ArrayList<>();
    }

    /**
     * 将分组后的数据存入集合并返回
     *
     * @param aggregations
     */
    private List<String> getBucketKey(Aggregations aggregations, String aggName) {
        //注意使用接口的子类，才能调用特有的方法
        Terms cityAgg = aggregations.get(aggName);
        //创建集合存储分组后的数据
        List<String> cities = new ArrayList<>();
        if (cityAgg != null) {
            List<? extends Terms.Bucket> buckets = cityAgg.getBuckets();
            if (buckets != null && buckets.size() > 0) {
                for (Terms.Bucket bucket : buckets) {
                    String key = (String) bucket.getKey();
                    //将每一条分组数据存入
                    cities.add(key);
                }
            }
        }
        return cities;
    }

    /**
     * 对查询出来的数据进行聚合分组
     * @param request
     * @param terms
     * @param field
     */
    private void aggregationGroup(SearchRequest request, String terms, String field) {
        //对数据进行分组（聚合）
        request.source().aggregation(AggregationBuilders
                //设置桶的标识
                .terms(terms)
                //聚合的字段
                .field(field)
                //栏目显示条数
                .size(10)
                //按照酒店数量降序排列
                .order(BucketOrder.count(false)));
    }


    /**
     * 对请求条件进行封装
     */
    private void requestCondition(RequestDto params, SearchRequest request) {
        //2.设置模糊查询条件
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        String key = params.getKey();
        //如果输入框为空,则查询所有
        if (StringUtils.isBlank(key)) {
            boolQuery.must(
                    QueryBuilders.matchAllQuery()
            );
        } else {
            boolQuery.must(
                    QueryBuilders.matchQuery("all", key));
        }

        //设置过滤条件(城市、星级、品牌、价格)
        String city = params.getCity();
        if (StringUtils.isNotBlank(city)) {
            boolQuery.filter(QueryBuilders.termQuery("city", city));
        }
        String starName = params.getStarName();
        if (StringUtils.isNotBlank(starName)) {
            boolQuery.filter(QueryBuilders.termQuery("starName", starName));
        }
        String brand = params.getBrand();
        if (StringUtils.isNotBlank(brand)) {
            boolQuery.filter(QueryBuilders.termQuery("brand", brand));
        }
        Integer minPrice = params.getMinPrice();
        Integer maxPrice = params.getMaxPrice();
        if (minPrice != null && minPrice >= 0) {
            boolQuery.filter(QueryBuilders.rangeQuery("price").gte(minPrice));
        }
        if (maxPrice != null && maxPrice >= 0) {
            boolQuery.filter(QueryBuilders.rangeQuery("price").lte(maxPrice));
        }


        //广告算分
        String script = "return doc['adScore'].value";
        //使用算法函数，给广告加分
        FunctionScoreQueryBuilder functionQuery = QueryBuilders.functionScoreQuery(
                //构造算法查询函数
                new FunctionScoreQueryBuilder(
                        //上述的查询条件（算法查询范围）
                        boolQuery,
                        //function score的数组
                        new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
                                //其中的一个function score 元素
                                new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                                        //过滤条件，满足条件的设置权重
                                        QueryBuilders.termQuery("isAD", true),
                                        //设置权重
                                        //ScoreFunctionBuilders.weightFactorFunction(10)
                                        //设置自定义值
                                        ScoreFunctionBuilders.scriptFunction(new String(script))
                                )})
                        //设置计算权重方式（乘法MULTIPLY）
                        .boostMode(CombineFunction.MULTIPLY)
        );

        //经过算法和条件后
        request.source().query(functionQuery);

        //排序
        //对地理坐标进行排序
        String location = params.getLocation();
        if (StringUtils.isNotBlank(location)) {
            request.source().sort(SortBuilders
                    //这里使用GeoPoint的构造方法，直接给坐标会有问题(需要在里面new)
                    .geoDistanceSort("location", new GeoPoint(location))
                    .order(SortOrder.ASC)
                    .unit(DistanceUnit.KILOMETERS));
        }
        //如果排序字段不为空排序
        String sortBy = params.getSortBy();
//        if(StringUtils.isNotBlank(sortBy)){
//            //按照评分进行降序排序
//            if (sortBy.equals("score")){
//                request.source().sort(sortBy,SortOrder.DESC);
//            }
//            //按照价格进行升序排序
//            if(sortBy.equals("price")){
//                request.source().sort(sortBy,SortOrder.ASC);
//            }
//        }
        //前端传来price-ASC/DESC
        if (StringUtils.isNotBlank(sortBy) && !sortBy.equals("default")) {
            String[] split = sortBy.split("-");
            //排序按照自己输入的的字符串
            request.source().sort(split[0], SortOrder.fromString(split[1]));
        }

        //3.设置分页
        request.source().from((params.getPage() - 1) * params.getSize());
        request.source().size(params.getSize());

        //4.设置高亮,三要素: 字段,前缀(<em>),后缀(</em>)(默认)
        request.source()
                .highlighter(new HighlightBuilder().field("name")
                        .requireFieldMatch(false)
                        .preTags("<em>")
                        .postTags("</em>"));
    }


    /**
     * 根据id从es索引库中删除对应数据
     * @param id
     */
    @Override
    public void deleteById(Long id) {
        //获取删除请求对象
        DeleteRequest request = new DeleteRequest(es.getIndexName());
        //设置条件
        request.id(id.toString());
        //执行操作
        try {
            client.delete(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("数据删除失败，失败原因{}",e.getMessage());
        }
    }

    /**
     * 根据id从数据库中查询到对应数据，同时添加到es索引库
     * @param id
     */
    @Override
    public void insertById(Long id) {
        //1.从数据库中查询数据
        Hotel hotel = this.getById(id);

        //2.创建请求对象(尽量保证es的文档id和数据库一致)
        IndexRequest request = new IndexRequest(es.getIndexName());

        //3.准备json格式数据
        request.id(hotel.getId().toString());
        String json = JSON.toJSONString(new HotelDoc(hotel));
        request.source(json, XContentType.JSON);
        try {
            //4.发送请求(文档添加请求)
            client.index(request, RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error("数据添加失败，失败原因{}",e.getMessage());
        }
    }
}
